import framebuf
from machine import Pin, SPI
import time

# ILI9163C Command Definitions
ILI9163C_SWRESET = 0x01  # Software Reset
ILI9163C_SLPOUT = 0x11   # Sleep Out
ILI9163C_COLMOD = 0x3A   # Color Mode
ILI9163C_GAMSET = 0x26   # Gamma Set
ILI9163C_DISPON = 0x29   # Display ON
ILI9163C_CASET = 0x2A    # Column Address Set
ILI9163C_RASET = 0x2B    # Row Address Set
ILI9163C_RAMWR = 0x2C    # RAM Write
ILI9163C_MADCTL = 0x36   # Memory Access Control
ILI9163C_INVCOL = 0x21   # Display Inversion On/Off
ILI9163C_GMCTRP1 = 0xE0  # Positive Gamma Correction
ILI9163C_GMCTRN1 = 0xE1  # Negative Gamma Correction
ILI9163C_FRMCTR1 = 0xB1  # Frame Rate Control (Normal Mode)
ILI9163C_INVCTR = 0xB4   # Display Inversion Control
ILI9163C_PWCTR1 = 0xC0   # Power Control 1
ILI9163C_PWCTR2 = 0xC1   # Power Control 2
ILI9163C_VMCTR1 = 0xC5   # VCOM Control 1
ILI9163C_VMCTR2 = 0xC7   # VCOM Control 2 (Offset)

# Color Definitions (RGB565 format)
BLACK = 0x0000
WHITE = 0xFFFF

# Inherit from framebuf.FrameBuffer
class ILI9163C_HwSPI(framebuf.FrameBuffer): 
    def __init__(self, spi_id, sck_pin, mosi_pin, cs_pin, rst_pin=None, width=128, height=128, rot=0, bgr=False, mhz=40, polarity=0, phase=0):
        """
        MicroPython driver for ILI9163C LCD using hardware 3-wire SPI.
        This version inherits from framebuf.FrameBuffer, allowing direct drawing.
        It performs manual bit-packing of 9-bit data into 8-bit SPI frames
        to work around the controller's non-native 9-bit SPI support, using the provided packing scheme.

        :param spi_id: SPI peripheral ID (e.g., 1 or 2 on a microcontroller)
        :param sck_pin: GPIO pin number for Serial Clock (SCL)
        :param mosi_pin: GPIO pin number for Master Out Slave In (MOSI/SDA)
        :param cs_pin: GPIO pin number for Chip Select (CS)
        :param rst_pin: Optional GPIO pin number for Reset (RST)
        :param width: Display width in pixels (default 128)
        :param height: Display height in pixels (default 128)
        :param rot: Display rotation (0, 1, 2, 3)
        :param bgr: True for BGR color order, False for RGB (default False)
        :param mhz: SPI clock speed in MHz (default 40)
        :param polarity: SPI Clock Polarity (CPOL). 0 or 1.
        :param phase: SPI Clock Phase (CPHA). 0 or 1.
        """
        self.width = width
        self.height = height
        self.rot = rot
        self.bgr = bgr

        # Initialize CS and RST pins
        self.cs = Pin(cs_pin, Pin.OUT, value=1) # CS high (inactive) initially
        self.rst = Pin(rst_pin, Pin.OUT, value=1) if rst_pin else None # RST high (not in reset) initially

        # Initialize Hardware SPI for 8-bit transfers
        self.spi = SPI(
            spi_id,
            baudrate=mhz * 1000 * 1000,
            polarity=polarity,
            phase=phase,
            sck=Pin(sck_pin),
            mosi=Pin(mosi_pin),
            bits=8,      # Standard 8-bit transfers
            firstbit=SPI.MSB # MSB first
        )

        # Create a FrameBuffer for drawing operations
        self.buffer = bytearray(self.width * self.height * 2) # 2 bytes per pixel for RGB565
        
        # Initialize the FrameBuffer superclass
        super().__init__(self.buffer, self.width, self.height, framebuf.RGB565)

        self.reset()
        self._init_display()

        # Clear display after initialization
        self.fill(BLACK) # Now calls the inherited fill method
        self.show()

    def _write(self, value, is_data):
        """
        Internal function to handle a single 9-bit logical write using 8-bit hardware SPI.
        Packs the 9-bit value into two 8-bit bytes as per the user's example.
        """
        # Form the 9-bit logical packet: D/C bit (MSB of the 9-bit value) + 8-bit value
        nine_bit_val = (0x100 | value) if is_data else (0x000 | value)
        # Pack the 9-bit value into two 8-bit bytes
        # Byte1: D/C, d7, d6, d5, d4, d3, d2, d1
        byte1 = (nine_bit_val >> 1) & 0xFF
        # Byte2: d0 (MSB), 0, 0, 0, 0, 0, 0, 0 (rest are padding)
        byte2 = (nine_bit_val & 0x01) << 7

        self.cs(0) # Assert CS (pull low)
        self.spi.write(bytearray([byte1, byte2]))
        self.cs(1) # De-assert CS (pull high)

    def _pack_9bit_values_into_8bit_bytes(self, nine_bit_values_list):
        """
        Packs a list of 9-bit integer values into a bytearray using a continuous bit stream,
        following the specified interleaving pattern:
        dc 7 6 5 4 3 2 1 | 0 dc 7 6 5 4 3 2 | 1 0 dc 7 6 5 4 3 ...
        """
        packed_bytes = bytearray()
        bit_buffer = 0
        bits_in_buffer = 0

        for val_9bit in nine_bit_values_list:
            # Ensure val_9bit is treated as 9 bits
            val_9bit &= 0x1FF 

            # Shift the new 9-bit value into the most significant end of the buffer
            bit_buffer = (bit_buffer << 9) | val_9bit
            bits_in_buffer += 9

            # While there are enough bits to form an 8-bit byte, extract from the MSB end
            while bits_in_buffer >= 8:
                # Extract the most significant 8 bits
                byte_to_add = (bit_buffer >> (bits_in_buffer - 8)) & 0xFF
                packed_bytes.append(byte_to_add)
                
                # Clear the extracted bits from the buffer by masking
                bit_buffer &= ((1 << (bits_in_buffer - 8)) - 1)
                
                bits_in_buffer -= 8
        
        # After the loop, if there are remaining bits (less than 8),
        # left-align them in the last byte and append.
        if bits_in_buffer > 0:
            packed_bytes.append((bit_buffer << (8 - bits_in_buffer)) & 0xFF)
            
        return packed_bytes

    def _write_cmd(self, cmd):
        """Sends a command byte to the display."""
        self._write(cmd, is_data=False)

    def _write_data(self, data):
        """Sends a data byte or bytearray to the display using bit-packing."""
        if isinstance(data, int):
            # For a single integer data, use the _write method (packs to two 8-bit bytes)
            self._write(data, is_data=True)
        elif isinstance(data, (bytes, bytearray)):
            # Convert each 8-bit data byte into a 9-bit logical value (D/C=1 for data)
            nine_bit_values = [(0x100 | byte_val) for byte_val in data] 
            packed_data = self._pack_9bit_values_into_8bit_bytes(nine_bit_values)
            
            self.cs(0) # Assert CS for the entire transfer
            self.spi.write(packed_data)
            self.cs(1) # De-assert CS after transfer
        else:
            raise ValueError("Data must be an int, bytes, or bytearray")

    def reset(self):
        """Performs a hardware reset on the display."""
        if self.rst:
            self.rst(1)
            time.sleep_ms(50)
            self.rst(0)
            time.sleep_ms(50)
            self.rst(1)
            time.sleep_ms(150)
        else:
            # If no RST pin, try a software reset
            self._write_cmd(ILI9163C_SWRESET)
            time.sleep_ms(150)

    def _init_display(self):
        """Initializes the display with a common sequence of commands."""
        self._write_cmd(ILI9163C_SWRESET) # Software reset
        time.sleep_ms(150)

        self._write_cmd(ILI9163C_SLPOUT) # Exit sleep
        time.sleep_ms(150)

        # --- Power Control settings (common for ILI9163C) ---
        self._write_cmd(ILI9163C_PWCTR1) # Power Control 1
        self._write_data(0x06) # GVDD = 4.7V, DDVDH = VCIx2
        self._write_cmd(ILI9163C_PWCTR2) # Power Control 2
        self._write_data(0x01) # VGH/VGL

        self._write_cmd(ILI9163C_VMCTR1) # VCOM Control 1
        self._write_data(0x30) # VCOM = 0.92V
        self._write_cmd(ILI9163C_VMCTR2) # VCOM Control 2
        self._write_data(0xC1) # VCOM Offset

        # --- Frame Rate Control ---
        self._write_cmd(ILI9163C_FRMCTR1) # Frame Rate Control (Normal Mode)
        self._write_data(0x05) # 105Hz
        self._write_cmd(ILI9163C_INVCTR) # Display Inversion Control
        self._write_data(0x03) # Column + Row inversion

        # --- Memory Access Control (MADCTL) ---
        madctl = 0x00
        if self.rot == 0:
            madctl = 0x08 # MY=0 MX=0 MV=0 ML=0 RGB=1 (Normal)
        elif self.rot == 1:
            madctl = 0xA8 # MY=1 MX=0 MV=1 ML=0 RGB=1 (Rotate 90)
        elif self.rot == 2:
            madctl = 0xC8 # MY=1 MX=1 MV=0 ML=0 RGB=1 (Rotate 180)
        elif self.rot == 3:
            madctl = 0x68 # MY=0 MX=1 MV=1 ML=0 RGB=1 (Rotate 270)

        if self.bgr:
            madctl |= 0x08 # BGR Bit (bit 3) if needed for your specific panel
            # Note: For ILI9163C, the BGR bit is typically MADCTL[3]. Some panels
            # might require 0x08 for RGB and 0x00 for BGR, or vice-versa.
            # You might need to experiment if colors are off.

        self._write_cmd(ILI9163C_MADCTL)
        self._write_data(madctl)

        # --- Color Mode ---
        self._write_cmd(ILI9163C_COLMOD)
        self._write_data(0x05) # 16-bit color (RGB565)

        # --- Gamma Set ---
        self._write_cmd(ILI9163C_GAMSET)
        self._write_data(0x01) # Gamma Curve 1 (Default)

        # --- Positive Gamma Correction ---
        self._write_cmd(ILI9163C_GMCTRP1)
        self._write_data(bytearray([0x3F, 0x25, 0x1C, 0x1E, 0x20, 0x12, 0x2A, 0x90, 0x08, 0x18, 0x0F, 0x11, 0x02, 0x02, 0x00]))

        # --- Negative Gamma Correction ---
        self._write_cmd(ILI9163C_GMCTRN1)
        self._write_data(bytearray([0x00, 0x18, 0x15, 0x02, 0x0F, 0x04, 0x0C, 0x36, 0x03, 0x06, 0x0B, 0x05, 0x09, 0x0F, 0x07]))

        self._write_cmd(ILI9163C_DISPON) # Display On
        time.sleep_ms(100)

    def _set_window(self, x0, y0, x1, y1):
        """
        Sets the active window for pixel operations.
        Note: Some 128x128 ILI9163C panels might have a small hardware offset
        (e.g., 2 pixels) for X and Y coordinates. Adjust x_offset/y_offset if needed.
        """
        x_offset = 0 # Adjust if your display has an X offset
        y_offset = 0 # Adjust if your display has an Y offset

        self._write_cmd(ILI9163C_CASET)  # Column address set
        self._write_data(bytearray([(x0 + x_offset) >> 8, (x0 + x_offset) & 0xFF]))
        self._write_data(bytearray([(x1 + x_offset) >> 8, (x1 + x_offset) & 0xFF]))

        self._write_cmd(ILI9163C_RASET)  # Row address set
        self._write_data(bytearray([(y0 + y_offset) >> 8, (y0 + y_offset) & 0xFF]))
        self._write_data(bytearray([(y1 + y_offset) >> 8, (y1 + y_offset) & 0xFF]))

        self._write_cmd(ILI9163C_RAMWR)  # Memory write (prepares for pixel data)

    def show(self):
        """Pushes the framebuffer content to the display using bit-packing."""
        self._set_window(0, 0, self.width - 1, self.height - 1)
        
        # Convert each 8-bit byte from the framebuffer into a 9-bit logical data value (D/C=1)
        # Note: self.buffer contains 2 bytes per RGB565 pixel.
        # We need to process each of these 8-bit bytes as data for the display.
        nine_bit_values_to_send = [(0x100 | byte_val) for byte_val in self.buffer] 
        packed_data = self._pack_9bit_values_into_8bit_bytes(nine_bit_values_to_send)
        
        self.cs(0) # Assert CS for the entire framebuffer data transfer
        self.spi.write(packed_data)
        self.cs(1) # De-assert CS after transfer is complete

    @staticmethod
    def rgb565(r, g, b, little_endian=True):
        color = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)
        return (color & 0xFF) << 8 | (color >> 8) if little_endian else color

# All framebuffer drawing methods (fill, pixel, text, rect, etc.) are inherited from framebuf.FrameBuffer.
